#include "stdafx.h"
#include "scene/mesh.h"
#include "core/logger.h"

namespace natrium {
	namespace scene {
		Mesh::Mesh(
			const std::string& filename, 
			bool reCenterMesh,
			bool loadTexCoords, 
			bool generateTangents
		):
			mReCenterMesh(reCenterMesh),
			mLoadTexCoords(loadTexCoords),
			mGenerateTangents(generateTangents)
		{
			loadObj(filename);
		}

		void Mesh::render() const {
			glBindVertexArray(mHandle);
			glDrawElements(GL_TRIANGLES, 3 * mNumElements, GL_UNSIGNED_INT, nullptr);
		}

		void Mesh::loadObj(const std::string& filename) {
			std::vector<glm::vec3> vertices;
			std::vector<glm::vec3> normals;
			std::vector<glm::vec2> texCoords;
			std::vector<unsigned int> indices;

			size_t numFaces = 0;

			std::ifstream input(filename.c_str());

			if (!input.good()) {
				gLog.error() << "Failed to open OBJ: " << filename;
				return;
			}

			std::string line;
			std::string token;
			std::vector<int> face;

			while (std::getline(input, line)) {
				boost::algorithm::trim(line);

				if (line.length() > 0) {
					std::istringstream lineStream(line);

					lineStream >> token;

					if (token == "v") {
						float x;
						float y;
						float z;

						lineStream >> x >> y >> z;

						vertices.emplace_back(x, y, z);
					}

					if (token == "vt") {
						float u;
						float v;

						lineStream >> u >> v;

						texCoords.emplace_back(u, v);
					}

					if (token == "vn") {
						float nx;
						float ny;
						float nz;

						lineStream >> nx >> ny >> nz;

						normals.emplace_back(nx, ny, nz);
					}

					if (token == "f") {
						++numFaces;

						face.clear();
						size_t slash1;
						size_t slash2;

						//ugh... various syntax options possible, try to catch em all :P
						while (lineStream.good()) {
							std::string vtx;
						
							lineStream >> vtx;

							int vertexRef = -1;
							int normalRef = -1;
							int texRef = -1;

							slash1 = vtx.find('/');
						
							if (slash1 == std::string::npos)
								vertexRef = std::atoi(vtx.c_str()) - 1; //only a single number is given; immediately compensate for counting from 1 instead of 0
							else {
								slash2 = vtx.find('/', slash1 + 1);

								vertexRef = std::atoi(vtx.substr(0, slash1).c_str()) - 1;

								if (slash2 > slash1 + 1)
									texRef = std::atoi(vtx.substr(slash1 + 1, slash2).c_str()) - 1;

								normalRef = std::atoi(vtx.substr(slash2 + 1).c_str()) - 1;
							}

							if (vertexRef == -1)
								gLog.warning() << "Missing vertex reference";
							else
								face.push_back(vertexRef);

							//I'm assuming a single index buffer can be used for vertex, normal and texcoord references
							//strictly speaking, this does not *have* to be the case, so I'll just dump lots of warnings
							//if it isn't so
							if (mLoadTexCoords && (texRef != -1) && (vertexRef != texRef))
								gLog.warning() << "Inconsistent texture reference";

							if ((normalRef != -1) && (vertexRef != normalRef))
								gLog.warning() << "Inconsistent normal reference";
						}

						//decompose faces larger than a triangle into a triangle fan (or should I treat it as a strip?)
						if (face.size() > 3) {
							unsigned int v0 = face[0];
							unsigned int v1 = face[1];
							unsigned int v2 = face[2];

							indices.push_back(v0);
							indices.push_back(v1);
							indices.push_back(v2);

							for (size_t i = 3; i < face.size(); ++i) {
								//cycle to the next
								v1 = v2;
								v2 = face[i];

								indices.push_back(v0);
								indices.push_back(v1);
								indices.push_back(v2);
							}
						}
						else {
							indices.push_back(face[0]);
							indices.push_back(face[1]);
							indices.push_back(face[2]);
						}
					}
				}
			}

			input.close();

			if (normals.empty())
				normals = generateNormals(vertices, indices);

			std::vector<glm::vec4> tangents;
		
			if (mGenerateTangents && !texCoords.empty())
				tangents = generateTangents(vertices, normals, texCoords, indices);

			if (mReCenterMesh)
				center(vertices);

			createVBO(vertices, normals, texCoords, indices, tangents);

			gLog.info() << "Loaded " << filename;
		}

		void Mesh::center(std::vector<glm::vec3>& vertices) const {
			if (vertices.empty())
				return;

			glm::vec3 boundMin = vertices[0];
			glm::vec3 boundMax = vertices[1];

			for (const auto& vtx: vertices) {
				if (vtx.x > boundMax.x)
					boundMax.x = vtx.x;
				if (vtx.x < boundMin.x)
					boundMin.x = vtx.x;

				if (vtx.y > boundMax.y)
					boundMax.y = vtx.y;
				if (vtx.y < boundMin.y)
					boundMin.y = vtx.y;

				if (vtx.z > boundMax.z)
					boundMax.z = vtx.z;
				if (vtx.z < boundMin.z)
					boundMin.z = vtx.z;
			}

			glm::vec3 aabbcenter = glm::vec3(
				(boundMin.x + boundMax.x) * 0.5f,
				(boundMin.y + boundMax.y) * 0.5f,
				(boundMin.z + boundMax.z) * 0.5f
			);

			for (auto& vtx: vertices)
				vtx = vtx - aabbcenter;
		}

		std::vector<glm::vec3> Mesh::generateNormals(const std::vector<glm::vec3>& vertices, const std::vector<unsigned int>& indices) const {
			std::vector<glm::vec3> result(vertices.size(), glm::vec3(0.0f));

			for (size_t i = 0; i < indices.size(); i += 3) {
				const auto& vtxA = vertices[indices[i + 0]];
				const auto& vtxB = vertices[indices[i + 1]];
				const auto& vtxC = vertices[indices[i + 2]];

				glm::vec3 a = vtxB - vtxA;
				glm::vec3 b = vtxC - vtxA;
				glm::vec3 n = glm::normalize(glm::cross(a, b));

				//accumulate referenced normals 
				result[indices[i + 0]] += n;
				result[indices[i + 1]] += n;
				result[indices[i + 2]] += n;
			}

			//average the accumulated normals
			for (auto& n: result)
				n = glm::normalize(n);

			return result;
		}

		std::vector<glm::vec4> Mesh::generateTangents(const std::vector<glm::vec3>& vertices, const std::vector<glm::vec3>& normals, const std::vector<glm::vec2>& texCoords, const std::vector<unsigned int>& indices) const {
			std::vector<glm::vec3> tangents1(vertices.size(), glm::vec3(0.0f));
			std::vector<glm::vec3> tangents2(vertices.size(), glm::vec3(0.0f));
			std::vector<glm::vec4> result(vertices.size(), glm::vec4(0.0f));

			//accumulate tangents for all triangles
			for (size_t i = 0; i < indices.size(); i += 3) {
				const auto& vtxA = vertices[indices[i + 0]];
				const auto& vtxB = vertices[indices[i + 1]];
				const auto& vtxC = vertices[indices[i + 2]];

				const auto& texA = texCoords[indices[i + 0]];
				const auto& texB = texCoords[indices[i + 1]];
				const auto& texC = texCoords[indices[i + 2]];

				glm::vec3 a = vtxB - vtxA;
				glm::vec3 b = vtxC - vtxA;

				float s1 = texB.x - texA.x;
				float s2 = texC.x - texA.x;
				float t1 = texB.y - texA.y;
				float t2 = texC.y - texA.y;

				float r = 1.0f / (s1 * t2 - s2 * t1);

				glm::vec3 tan1(
					(t2 * a.x - t1 * b.x) * r,
					(t2 * a.y - t1 * b.y) * r,
					(t2 * a.z - t1 * b.z) * r
				);

				glm::vec3 tan2(
					(s1 * b.x - s2 * b.x) * r,
					(s1 * b.y - s2 * b.y) * r,
					(s1 * b.z - s2 * b.z) * r
				);

				tangents1[indices[i + 0]] += tan1;
				tangents1[indices[i + 1]] += tan1;
				tangents1[indices[i + 2]] += tan1;

				tangents2[indices[i + 0]] += tan2;
				tangents2[indices[i + 1]] += tan2;
				tangents2[indices[i + 2]] += tan2;
			}

			for (size_t i = 0; i < vertices.size(); ++i) {
				const auto& normal = normals[i];
				const auto& tan1 = tangents1[i];
				const auto& tan2 = tangents2[i];

				//Gram-Schmidt orthogonalize
				result[i] = glm::vec4(glm::normalize(tan1 - (glm::dot(normal, tan1) * normal)), 0.0f);

				//store the handedness in the w component
				if (glm::dot(glm::cross(normal, tan1), tan2) < 0.0f)
					result[i].w = -1.0f;
				else
					result[i].w = 1.0f;
			}

			return result;
		}

		void Mesh::createVBO(
			const std::vector<glm::vec3>& vertices, 
			const std::vector<glm::vec3>& normals, 
			const std::vector<glm::vec2>& texCoords,
			const std::vector<unsigned int>& indices,
			const std::vector<glm::vec4>& tangents
		) {
			size_t numVertices = vertices.size();
			mNumElements = indices.size() / 3;

			//copy vectors into plain arrays (not sure if glm::vec3 can be considered as a POD)
			float* vtx = new float[3 * numVertices];
			float* nrm = new float[3 * numVertices];
			float* tex = nullptr;
			float* tng = nullptr;

			if (!texCoords.empty()) {
				tex = new float[2 * numVertices];

				if (!tangents.empty())
					tng = new float[4 * numVertices];
			}

			for (size_t i = 0; i < numVertices; ++i) {
				vtx[i * 3 + 0] = vertices[i].x;
				vtx[i * 3 + 1] = vertices[i].y;
				vtx[i * 3 + 2] = vertices[i].z;

				nrm[i * 3 + 0] = normals[i].x;
				nrm[i * 3 + 1] = normals[i].y;
				nrm[i * 3 + 2] = normals[i].z;

				if (tex) {
					tex[i * 2 + 0] = texCoords[i].x;
					tex[i * 2 + 1] = texCoords[i].y;
				}

				if (tng) {
					tng[i * 4 + 0] = tangents[i].x;
					tng[i * 4 + 1] = tangents[i].y;
					tng[i * 4 + 2] = tangents[i].z;
					tng[i * 4 + 3] = tangents[i].w;
				}
			}

			//now allocate the openGL handles and move the data to the gpu
			glGenVertexArrays(1, &mHandle);
			glBindVertexArray(mHandle);

			GLuint vertexBuffer;
			GLuint normalBuffer;
			GLuint texCoordBuffer;
			GLuint indexBuffer;
			GLuint tangentBuffer;

			//start with the vertices
			glGenBuffers(1, &vertexBuffer);
			glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
			glBufferData(GL_ARRAY_BUFFER, 3 * numVertices * sizeof(float), vtx, GL_STATIC_DRAW);
			glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr); 
			glEnableVertexAttribArray(0); //vertex position at location 0

			//continue with the normals
			glGenBuffers(1, &normalBuffer);
			glBindBuffer(GL_ARRAY_BUFFER, normalBuffer);
			glBufferData(GL_ARRAY_BUFFER, 3 * numVertices * sizeof(float), nrm, GL_STATIC_DRAW);
			glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
			glEnableVertexAttribArray(1); //normals at location 1

			if (tex) {
				glGenBuffers(1, &texCoordBuffer);
				glBindBuffer(GL_ARRAY_BUFFER, texCoordBuffer);
				glBufferData(GL_ARRAY_BUFFER, 2 * numVertices * sizeof(float), tex, GL_STATIC_DRAW);
				glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
				glEnableVertexAttribArray(2); //texCoords at location 2
			}

			if (tng) {
				glGenBuffers(1, &tangentBuffer);
				glBindBuffer(GL_ARRAY_BUFFER, tangentBuffer);
				glBufferData(GL_ARRAY_BUFFER, 4 * numVertices * sizeof(float), tng, GL_STATIC_DRAW);
				glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
				glEnableVertexAttribArray(3); //tangents at location 3
			}

			glGenBuffers(1, &indexBuffer);
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
			glBufferData(GL_ELEMENT_ARRAY_BUFFER, 3 * mNumElements * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW);

			glBindVertexArray(0);

			delete[] vtx;
			delete[] nrm;
		
			if (tex)
				delete[] tex;
		
			if (tng)
				delete[] tng;
		}
	}
}